[update] Amazon CloudFrontでリクエストのヘッダ構造を判断するためのヘッダが利用可能になりました!
はじめに
清水です。AWSのCDNサービスであるAmazon CloudFrontでViewer(リクエスト)のヘッダ構造を判断するためのヘッダが利用可能になりました!(2023/01/13付でAWS What's Newにポストされたアップデート情報となります。)
- Amazon CloudFront now supports the request header order and header count headers
- Amazon CloudFront がリクエストしたヘッダーの順序とヘッダー数の各ヘッダーのサポートを開始
具体的には、リクエスト時に以下2つのヘッダがCloudFrontで付与できるようになりました。
CloudFront-Viewer-Header-Order
CloudFront-Viewer-Header-Count
1つ目のCloudFront-Viewer-Header-Order
はViewer(リクエスト)のヘッダの名称がコロンで区切られたかたちで格納されます。例えばCloudFront-Viewer-Header-Order: Host:User-Agent:Accept:Accept-Encoding
というぐあいですね。(Host:User-Agent:Accept:Accept-Encoding
がCloudFront-Viewer-Header-Order
ヘッダの値というわけです。)
2つ目のCloudFront-Viewer-Header-Count
はViewer(リクエスト)のヘッダの総数が数値で格納されます。先ほどの例であればヘッダの総数が5つなのでCloudFront-Viewer-Header-Count: 5
となります。
これら2つのヘッダを用いてHTTPヘッダの順序と総数が追跡できるようになり、クライアントからのリクエストを期待される正当なパターンと比較することができます。ほかのアクセス制御ルールと組み合わせることで、リクエストの偽造を検知しブロックすることなどに役立てることが可能ということです。
なお、Amazon CloudFront Developer Guideのほうも同じく2023/01/13付で更新されており、「Adding the CloudFront request headers - Amazon CloudFront」のページに「Headers for determining the viewer's header structure」という項目が追加されています。
本エントリでは、このCloudFrontでViewerのヘッダ構造を判断するためのヘッダについて、実際に動作を確認してみたのでまとめてみます。
re:Invent 2022のセッション内でアップデートが先行発表されていた!!
さて動作確認の前ですが、これら2つのアップデート対象となるヘッダ、CloudFront-Viewer-Header-Order
とCloudFront-Viewer-Header-Count
をみてピンと来た方もいるかと思います。この2つのヘッダについては、昨年末のre:Invent 2022のセッション「Optimizing performance with CloudFront: Every millisecond matters (NET313)」にてWhat's Newへのポストに先行して紹介があったアップデートとなります。
以下の該当セッションのレポートブログ執筆時(2022年12月)にも、2つのヘッダはOrigin request policy作成時のカスタムヘッダとして個別に追加することにより利用自体は可能だったのですが、今回正式にアップデート情報として案内されたかたちです。また本ブログエントリで紹介しますが、Origin request policyでのヘッダ追加時にきちんと選択ができるようにもなっています。
CloudFrontでViewerのヘッダ構造を判断するためのヘッダを使ってみた
では実際に、今回のアップデート内容となるViewer(リクエスト)のヘッダ構造を判断するための2つのヘッダについて、実際にCloudFrontに設定してその挙動を確認してみます。使い方はほかのCloudFront HTTPヘッダーの利用方法と同様で、Origin request policyにヘッダを追加するかたちですすめていきます。
オリジンサーバの準備
CloudFrontに設定するオリジンとして、Apache HTTP ServerとPHPが稼働するEC2を準備しました。以下のPHPコードを記載したファイルindex.php
を配置し、デフォルトルート(/
)にアクセスすればオリジン側で確認したリクエストヘッダをすべて出力するようにします。
<?php foreach (getallheaders() as $name => $value) { echo "<p>"; echo "$name: $value"; echo "</p>\n"; } ?>
Origin request policyの作成
CloudFront側の設定として、まずはOrigin request policyを作成します。マネジメントコンソールのCloudFrontの画面、PoliciesのOrigin requestの項目から[Create origin request policy]で進みます。NameとDescriptionを適切に設定します。
Origin request settingsの項目ではHeadersで「All viewer headers and the following CloudFront headers」を選択しました。(All viewer headersは任意ですが、CloudFront headersを選択する必要があります。)Add headerでOrigin requestに含めるCloudFront headersとして、CloudFront-Viewer-Header-Order
とCloudFront-Viewer-Header-Count
を選択します。
Origin request policyが作成できました。
Distributionの作成
Origin request policyが作成できたら、続いてこのOrigin request policyを使用するCloudFront Distributionを作成します。Distribution一覧画面から[Create Distribution]ボタンで進みます。オリジンは先ほど準備したEC2のPublic IPv4 DNSを指定しました。その他は基本的にマネジメントコンソールのデフォルト設定で進みますが、Cache key and origin requestsの項目は以下のように設定しました。キャッシュを無効にしつつ、先ほど作成したOrigin request policyを使用するかたちですね。
Distributionが作成できたら、続いてCloudFront Functionsを設定します。
CloudFront Functionsの設定
オリジン側でリクエストヘッダをすべて出力するようにしていますが、今回はCloudFrontが受け取った段階のViewer(リクエスト)ヘッダの内容も確認したかったため、CloudFront Functionsでこれを出力するようにします。Functionのコードの内容は以下です。シンプルにconsole.log()
でevent.request
をログに出力するだけとなります。
function handler(event) { console.log(event.request); return event.request; }
CloudFrontマネジメントコンソールのFunctionsのページ、[Create functions]ボタンから進みます。NameとDescriptionを適切に設定します。
遷移後の画面でFunction codeのDevelopmentを上記のコードの内容で書き換え、[Save changes]します。
Publishのタブを開き、[Publish function]を行います。
Publishしたあと、Associated distributionの項目で[Add association]ボタンから先ほど作成したDistributionへの関連付けを行います。Event typeはViewer Request
としました。Cache behaviorはDefault (*)
に設定します。
Functionを関連付けたDistributionのDeployingか終了すれば準備完了です。
CloudFront-Viewer-Header-OrderヘッダならびにCloudFront-Viewer-Header-Countヘッダの確認
Functionを関連付けたDistributionのDeployingか終了すれば準備完了です。CloudFrontのDistribution domain nameにアクセスしてみましょう。
Safariブラウザでの確認
まずはmacOSのSafariブラウザで確認してみました。
アップデート対象である2つのヘッダの中身は以下となりました。
- Cloudfront-Viewer-Header-Order:
host:user-agent:accept:accept-language:accept-encoding
- Cloudfront-Viewer-Header-Count:
5
また実際にオリジンサーバから出力されるリクエストヘッダの内容は順にHost
、User-Agent
、X-Amz-Cf-Id
、Connection
、Via
、X-Forwarded-For
、Accept-Language
、Accept
、Accept-Encoding
の合計9個でした。
続いてCloudFront Functions側の出力、つまりCloudFrontが受け取った段階でのヘッダの内容についても確認してみます。CloudWatch Logsで確認してみると以下の内容が出力されていました。(見やすくなるように出力を整形しています。本エントリ内のほかのログ出力も同様です。)
T75MxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxqQ== { method:'GET', uri:'/', querystring:{}, headers:{ host:{value:'dgl3xxxxxxxxx.cloudfront.net'}, user-agent:{value:'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.2 Safari/605.1.15'}, accept:{value:'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'}, accept-language:{value:'ja'}, accept-encoding:{value:'gzip, deflate, br'}, cloudfront-viewer-header-order:{value:'host:user-agent:accept:accept-language:accept-encoding'}, cloudfront-viewer-header-count:{value:'5'} }, cookies:{} }
アップデート対象である2つのヘッダCloudfront-Viewer-Header-Order
とCloudfront-Viewer-Header-Count
について、その中身は同一ですね。この2つより前の順序となるヘッダについても確認してみましょう、host
、user-agent
、accept
、accept-language
、accept-encoding
とCloudfront-Viewer-Header-Order
の順序どおりに並んでいることがわかります。ヘッダの総数もCloudfront-Viewer-Header-Order
とCloudfront-Viewer-Header-Count
を除けば5つでCloudfront-Viewer-Header-Count
の値と一致しました。
オリジンサーバ側でのみ確認できたX-Amz-Cf-Id
、Connection
、Via
、X-Forwarded-For
などのヘッダについては、Viewerからのリクエスト時に存在しているヘッダではなく、CloudFrontがオリジンとの通信の際に付与するヘッダということになりますね。また順序については、Origini Request Policyの内容によってCloudFront側で置き換える可能性があるもの(今回ならHost
とUser-Agent
)が先になり、削除される可能性があるもの(今回ならAccept
、Accept-Language
、Accept-Encoding
はあととなる、というような規則なのかなと推測しています。(「HTTP リクエストヘッダーと CloudFront の動作 (カスタムオリジンおよび Amazon S3 オリジン)
」 カスタムオリジンの場合のリクエストおよびレスポンスの動作 - Amazon CloudFront)
curlコマンドでの確認
続いてcurlコマンドでも確認してみます。Safariブラウザの場合と同様、CloudWatch Logsで確認できるCloudFrontが受け取ったリクエストの段階のヘッダではCloudfront-Viewer-Header-Count
の個数のヘッダが、Cloudfront-Viewer-Header-Order
の順序で並んでいます。オリジンサーバで受け取るリクエストではこれらにCloudFrontが付与する各種ヘッダが加わり、また順序についても一部変更があるというぐあいですね。
% curl https://dgl3xxxxxxxxx.cloudfront.net <p>Host: dgl3xxxxxxxxx.cloudfront.net</p> <p>User-Agent: curl/7.79.1</p> <p>X-Amz-Cf-Id: McfZxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx8A==</p> <p>Connection: Keep-Alive</p> <p>Via: 2.0 c299xxxxxxxxxxxxxxxxxxxxxxxx62a2.cloudfront.net (CloudFront)</p> <p>X-Forwarded-For: 2axx:xxxx:xxxx:xx::xx:x7f</p> <p>Accept: */*</p> <p>Cloudfront-Viewer-Header-Order: host:user-agent:accept</p> <p>Cloudfront-Viewer-Header-Count: 3</p>
McfZxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx8A== { method:'GET', uri:'/', querystring:{}, headers:{ host:{value:'dgl3xxxxxxxxx.cloudfront.net'}, user-agent:{value:'curl/7.79.1'}, accept:{value:'*/*'}, cloudfront-viewer-header-order:{value:'host:user-agent:accept'}, cloudfront-viewer-header-count:{value:'3'} }, cookies:{} }
curlコマンドでリクエスト時に独自のヘッダを追加してみましょう。挙動などは独自ヘッダを付与しないときと変わりありませんが、Cloudfront-Viewer-Header-Count
がきちんと増えていることやCloudfront-Viewer-Header-Order
の順序も意図されたものとなっていることがわかります。
% curl https://dgl3xxxxxxxxx.cloudfront.net -H "x-myheader1: value1" -H "x-myheader2: value2" <p>Host: dgl3xxxxxxxxx.cloudfront.net</p> <p>User-Agent: curl/7.79.1</p> <p>X-Amz-Cf-Id: Lsa1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPg==</p> <p>Connection: Keep-Alive</p> <p>Via: 2.0 1e30xxxxxxxxxxxxxxxxxxxxxxxx7c88.cloudfront.net (CloudFront)</p> <p>X-Forwarded-For: 2axx:xxxx:xxxx:xx::xx:x7f</p> <p>Accept: */*</p> <p>X-Myheader1: value1</p> <p>X-Myheader2: value2</p> <p>Cloudfront-Viewer-Header-Order: host:user-agent:accept:x-myheader1:x-myheader2</p> <p>Cloudfront-Viewer-Header-Count: 5</p>
Lsa1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPg== { method:'GET', uri:'/', querystring:{}, headers:{ host:{value:'dgl3xxxxxxxxx.cloudfront.net'}, user-agent:{value:'curl/7.79.1'}, accept:{value:'*/*'}, x-myheader1:{value:'value1'}, x-myheader2:{value:'value2'}, cloudfront-viewer-header-order:{value:'host:user-agent:accept:x-myheader1:x-myheader2'}, cloudfront-viewer-header-count:{value:'5'} }, cookies:{} }
Chromeブラウザでの確認
さて、続いてはmacOSのChromeブラウザでも確認してみました。Chromeの場合はCloudfront-Viewer-Header-Count
の値が「13」とSafariに比べて多くなっていますね。詳細を確認してみると、CloudFrontがViewerからのリクエストを受け取った段階、CloudFront Functionsで確認できるヘッダでCloudfront-Viewer-Header-Order
の順序と異なった状態となっていました。(何度か条件を変えて確認しても同様の結果となったので、リクエストが偽装されているといったものではないかと思います。)
Safariと比べて追加されているのはUser-Agent Client Hints関連のヘッダとFetch Metadata Request Headersと呼ばれるヘッダ群などでしょうか。今回はこれら詳細などについて深追いはしませんが、Chromeブラウザでは現在のところCloudfront-Viewer-Header-Order
の扱いについては注意が必要そうです。
kSVwxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxfg== { method:'GET', uri:'/', querystring:{}, headers:{ user-agent:{value:'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'}, sec-ch-ua-mobile:{value:'?0'}, cloudfront-viewer-header-order:{value:'host:sec-ch-ua:sec-ch-ua-mobile:sec-ch-ua-platform:upgrade-insecure-requests:user-agent:accept:sec-fetch-site:sec-fetch-mode:sec-fetch-user:sec-fetch-dest:accept-encoding:accept-language'}, host:{value:'dgl3xxxxxxxxx.cloudfront.net'},accept:{value:'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9'}, upgrade-insecure-requests:{value:'1'}, sec-fetch-site:{value:'none'}, sec-fetch-dest:{value:'document'}, accept-language:{value:'ja,en-US;q=0.9,en;q=0.8'}, accept-encoding:{value:'gzip, deflate, br'}, sec-ch-ua-platform:{value:'"macOS"'}, sec-fetch-user:{value:'?1'}, cloudfront-viewer-header-count:{value:'13'}, sec-ch-ua:{value:'"Not_A Brand";v="99", "Google Chrome";v="109", "Chromium";v="109"'}, sec-fetch-mode:{value:'navigate'}}, cookies:{} }
まとめ
Amazon CloudFrontに新たに追加された2つのヘッダCloudFront-Viewer-Header-Order
ならびにCloudFront-Viewer-Header-Count
について、実際に設定して確認してみました。これらの2つのヘッダを用いることで、Viewerからのリクエスト時のヘッダの順序ならびのその個数が確認可能となります。ただし、オリジン側で参照する際にはCloudFront側で付与などを行うヘッダの影響や、Origin request policyの設定内容などを考慮する必要があります。CloudFront FunctionsやLambda@Edgeなどで参照するほうがシンプルに利用できそうですね。また今回確認した限りでは、一部ブラウザでヘッダ順序が維持されないケースがあった点についても注意して活用しましょう。